package client;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;

import server.Crypto;
import server.Message;
import server.Message.MsgType;

public class ClientLib {

	private static final int NUM_TRIES = 3;
	private static final int TIMEOUT = 1000;

	public interface OnSub {
		public void onSubReceive(Message m);
	}
	
	public enum ClientType {
		PUBLISHER, SUBSCRIBER;
	}
	
	public class FailedConnectException extends Exception {
		private static final long serialVersionUID = -1148952095628326650L;
		
	}

	private final String ENCODING;

	private DatagramChannel sock;
	public String myId;
	private long seq;
	private ClientType type;

	private InetSocketAddress server;
	private boolean connected = false;

	public ClientLib(String id, String host, int port, ClientType type) {
		this(id, host, port, "UTF-8", type);
	}

	public ClientLib(String id, String host, int port, String encoding, ClientType type) {
		this.ENCODING = encoding;
		try {
			this.myId = Crypto.hash(id);
			this.seq = 0;
			this.server = new InetSocketAddress(host, port);
			this.type = type;

			setupSocket();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public long getSeq() {
		return seq;
	}

	private void setupSocket() throws IOException {
		sock = DatagramChannel.open();
		sock.configureBlocking(false);
		sock.socket().setReceiveBufferSize(524288);
		sock.connect(server);
	}
	
	public void connect() throws IOException, FailedConnectException {
		connect("", 0);
	}

	public void connect(String data) throws IOException, FailedConnectException {
		connect(data, 0);
	}
	
	public void connect(String data, int tries) throws IOException, FailedConnectException {
		if (tries > 3)
			throw new FailedConnectException();
		
		Message m = new Message(MsgType.CONNECT, myId, seq++, data);
		if (type == ClientType.PUBLISHER)
			m.flags |= 0x1;
		
		sendMsg(m);

		Selector sel = Selector.open();
		sock.register(sel, SelectionKey.OP_READ);
		ByteBuffer buf = ByteBuffer.allocate(2048);
		sel.select(TIMEOUT);
		if (sel.selectedKeys().size() != 0) {
			for (SelectionKey s : sel.selectedKeys()) {
				if (s.isValid() && s.isReadable()) {
					DatagramChannel ch = ((DatagramChannel) s.channel());
					ch.receive(buf);
					sel.selectedKeys().remove(s);
					m = Message.deserialize(buf);
					System.out.println(m.flags);
					connected = (m.flags & 0x2) > 0;
					if (!connected) {
						String[] params = m.data.trim().toLowerCase().split(" ");
						System.err.println(params[0]);
						if (params[0].contains("/")) {
							System.err.println("Here we go!");
							params[0] = params[0].substring(0, params[0].indexOf('/'));
							System.err.println(params[0]);
						}
						server = new InetSocketAddress(params[0], Integer.parseInt(params[1]));
						setupSocket();
						connect(data, 0);
					}
				}
			}
		}
		else {
			System.out.println("timed out");
			connected = false;
			connect(data, tries + 1);
		}
	}

	private void sendMsg(Message m) throws IOException {
		sock.send(m.serialize(), server);
	}

	public void publish(byte[] data) throws IOException {
		if (!connected)
			throw new IOException("Not connected to service!");
		sendMsg(new Message(MsgType.PUB, myId, seq++, data));
	}

	public void publish(String data) throws UnsupportedEncodingException,
			IOException {
		publish(data.getBytes(ENCODING));
	}

	public void subscribe(final String pubId, final OnSub callback) throws IOException, FailedConnectException {
		connect(Crypto.hash(pubId));
		if (!connected)
			throw new IOException("Not connected to service!");
		else {
			new Thread() {
				
				@Override
				public void run() {
					try {
						Selector sel = Selector.open();
						sock.register(sel, SelectionKey.OP_READ);
						ByteBuffer buf = ByteBuffer.allocate(2048);
						while (true) {
							buf.clear();
							sel.select();
							for (SelectionKey s : sel.selectedKeys()) {
								if (s.isValid() && s.isReadable()) {
									buf.clear();
									DatagramChannel ch = ((DatagramChannel) s.channel());
									ch.read(buf);
									buf.flip();
									Message m = Message.deserialize(buf);
									if (m.type == MsgType.PUB)
										callback.onSubReceive(m);
									sel.selectedKeys().remove(s);
								}
							}
						}
					}
					catch (IOException e) {
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}
